package personal.service.accounting;

import java.math.BigDecimal;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import personal.dao.accounting.AccountDAO;
import personal.dao.accounting.ExternalEntityDAO;
import personal.dao.accounting.IncomeExpenseDAO;
import personal.dao.accounting.MonthwiseCalendarDAO;
import personal.dao.accounting.TransactionDAO;
import personal.dao.support.TransactionSearchCriteria;
import personal.domain.accounting.ExpenseTransaction;
import personal.domain.accounting.ExternalEntity;
import personal.domain.accounting.IncomeTransaction;
import personal.domain.accounting.MonthendAccountBalance;
import personal.domain.accounting.MonthlyCategoryTotal;
import personal.domain.accounting.MonthwiseCalendar;
import personal.domain.accounting.TransferTransaction;


@Service
public class TransactionServiceImpl implements TransactionService {
	private AccountDAO accountDAO;
	private MonthwiseCalendarDAO monthwiseCalendarDAO;
	private IncomeExpenseDAO incomeExpenseDAO;
	private ExternalEntityDAO externalEntityDAO;
	private TransactionDAO transactionDAO;

	public AccountDAO getAccountDAO() {
		return accountDAO;
	}

	@Autowired(required=false)
	public void setAccountDAO(AccountDAO accountDAO) {
		this.accountDAO = accountDAO;
	}

	public MonthwiseCalendarDAO getMonthwiseCalendarDAO() {
		return monthwiseCalendarDAO;
	}

	@Autowired(required=false)
	public void setMonthwiseCalendarDAO(MonthwiseCalendarDAO monthwiseCalendarDAO) {
		this.monthwiseCalendarDAO = monthwiseCalendarDAO;
	}

	public IncomeExpenseDAO getIncomeExpenseDAO() {
		return incomeExpenseDAO;
	}

	@Autowired(required=false)
	public void setIncomeExpenseDAO(IncomeExpenseDAO incomeExpenseDAO) {
		this.incomeExpenseDAO = incomeExpenseDAO;
	}

	public ExternalEntityDAO getExternalEntityDAO() {
		return externalEntityDAO;
	}

	@Autowired(required=false)
	public void setExternalEntityDAO(ExternalEntityDAO externalEntityDAO) {
		this.externalEntityDAO = externalEntityDAO;
	}

	public TransactionDAO getTransactionDAO() {
		return transactionDAO;
	}

	@Autowired(required=false)
	public void setTransactionDAO(TransactionDAO transactionDAO) {
		this.transactionDAO = transactionDAO;
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public ExpenseTransaction addExpenseTransaction(ExpenseTransaction transaction) {
		/** Add transaction **/
		ExpenseTransaction txn = transactionDAO.saveExpenseTransaction(transaction);
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
		/** Update income expense category monthly total for transaction month **/
		ExternalEntity entity = externalEntityDAO.getEntity(transaction.getEntityId());
		adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),transaction.getAmount());
		/** Update account balance from transaction month till now **/
		adjustMonthendBalanceSince(transaction.getAccountId(),transaction.getAmount().negate(),calendar.getCalendarId());
		return txn;
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public void deleteExpenseTransaction(ExpenseTransaction transaction) {
		/** Delete the transaction **/
		transactionDAO.deleteExpenseTransaction(transaction.getTxnId());
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
		/** Adjust monthend balances for account from transaction month onwards **/
		adjustMonthendBalanceSince(transaction.getAccountId(),transaction.getAmount(),calendar.getCalendarId());
		/** Adjust monthly total for income expense category for transaction month **/
		ExternalEntity entity = externalEntityDAO.getEntity(transaction.getEntityId());
		adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),transaction.getAmount().negate());
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRES_NEW)
	public void adjustExpenseTransaction(ExpenseTransaction transaction) {
		//Find the existing transaction
		TransactionSearchCriteria criteria = new TransactionSearchCriteria();
		criteria.setTxnId(transaction.getTxnId());
		ExpenseTransaction existingTransaction = transactionDAO.getExpenseTransactions(criteria).get(0);
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(existingTransaction.getTxnYear(), existingTransaction.getTxnMonth());
		/** Find entity **/
		ExternalEntity entity = externalEntityDAO.getEntity(existingTransaction.getEntityId());
		/** If txn month or year has changed adjust account balances in 2 steps
		 *  1a. Undo existing transaction amount since original transaction month & year account balance
		 *  1b. Undo existing transaction amount from category total of transaction month & year
		 *  1c. Apply new transaction amount to account balance since new transaction month and year 
		 *  1d. Apply new transaction amount to category total for new transaction month & year
		 *  Else 
		 *  2a. Apply differential amount to account balances since original transaction month & year account balance
		 *  2b. Apply differential amount to category total for new transaction month and year **/
		if ((transaction.getTxnMonth() != existingTransaction.getTxnMonth()) || (transaction.getTxnYear() != existingTransaction.getTxnYear())) {
			/** 1a **/
			adjustMonthendBalanceSince(existingTransaction.getAccountId(),existingTransaction.getAmount(),calendar.getCalendarId());
			/** 1b **/
			adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),existingTransaction.getAmount().negate());
			
			calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
			/** 1c **/
			adjustMonthendBalanceSince(transaction.getAccountId(),transaction.getAmount().negate(),calendar.getCalendarId());
			/** 1d **/
			adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),transaction.getAmount());
		} else {
			BigDecimal amountDiff = existingTransaction.getAmount().subtract(transaction.getAmount());
			adjustMonthendBalanceSince(existingTransaction.getAccountId(),amountDiff.negate(),calendar.getCalendarId());
			adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),amountDiff);
		}
		/** save transaction **/
		transactionDAO.saveExpenseTransaction(transaction);
	}

	@Override
	@Transactional(readOnly=true)
	public List<ExpenseTransaction> getExpenseTransactions(TransactionSearchCriteria criteria) {
		return transactionDAO.getExpenseTransactions(criteria);
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public IncomeTransaction addIncomeTransaction(IncomeTransaction transaction) {
		/** Add transaction **/
		IncomeTransaction txn = transactionDAO.saveIncomeTransaction(transaction);
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
		/** Update income expense category monthly total for transaction month **/
		ExternalEntity entity = externalEntityDAO.getEntity(transaction.getEntityId());
		adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),transaction.getAmount());
		/** Update account balance from transaction month till now **/
		adjustMonthendBalanceSince(transaction.getAccountId(),transaction.getAmount(),calendar.getCalendarId());
		return txn;
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public void deleteIncomeTransaction(IncomeTransaction transaction) {
		/** Delete the transaction **/
		transactionDAO.deleteIncomeTransaction(transaction.getTxnId());
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
		/** Adjust monthend balances for account from transaction month onwards **/
		adjustMonthendBalanceSince(transaction.getAccountId(),transaction.getAmount().negate(),calendar.getCalendarId());
		/** Adjust monthly total for income expense category for transaction month **/
		ExternalEntity entity = externalEntityDAO.getEntity(transaction.getEntityId());
		adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),transaction.getAmount().negate());
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRES_NEW)
	public void adjustIncomeTransaction(IncomeTransaction transaction) {
		//Find the existing transaction
		TransactionSearchCriteria criteria = new TransactionSearchCriteria();
		criteria.setTxnId(transaction.getTxnId());
		IncomeTransaction existingTransaction = transactionDAO.getIncomeTransactions(criteria).get(0);
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(existingTransaction.getTxnYear(), existingTransaction.getTxnMonth());
		/** Find entity **/
		ExternalEntity entity = externalEntityDAO.getEntity(existingTransaction.getEntityId());
		/** If txn month or year has changed adjust account balances in 2 steps
		 *  1a. Undo existing transaction amount since original transaction month & year account balance
		 *  1b. Undo existing transaction amount from category total of transaction month & year
		 *  1c. Apply new transaction amount to account balance since new transaction month and year 
		 *  1d. Apply new transaction amount to category total for new transaction month & year
		 *  Else 
		 *  2a. Apply differential amount to account balances since original transaction month & year account balance
		 *  2b. Apply differential amount to category total for new transaction month and year **/
		if ((transaction.getTxnMonth() != existingTransaction.getTxnMonth()) || (transaction.getTxnYear() != existingTransaction.getTxnYear())) {
			/** 1a **/
			adjustMonthendBalanceSince(existingTransaction.getAccountId(),existingTransaction.getAmount().negate(),calendar.getCalendarId());
			/** 1b **/
			adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),existingTransaction.getAmount().negate());
			
			calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
			/** 1c **/
			adjustMonthendBalanceSince(transaction.getAccountId(),transaction.getAmount(),calendar.getCalendarId());
			/** 1d **/
			adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),transaction.getAmount());
		} else {
			BigDecimal amountDiff = existingTransaction.getAmount().subtract(transaction.getAmount());
			adjustMonthendBalanceSince(existingTransaction.getAccountId(),amountDiff,calendar.getCalendarId());
			adjustMonthlyCategoryTotal(entity.getIeCategoryId(),calendar.getCalendarId(),amountDiff);
		}
		/** save transaction **/
		transactionDAO.saveIncomeTransaction(transaction);
	}

	@Override
	@Transactional(readOnly=true)
	public List<IncomeTransaction> getIncomeTransactions(TransactionSearchCriteria criteria) {
		return transactionDAO.getIncomeTransactions(criteria);
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public TransferTransaction addTransferTransaction(TransferTransaction transaction) {
		/** add transaction **/
		TransferTransaction txn = transactionDAO.saveTransferTransaction(transaction);
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
		/** Adjust balance of 'from' account from transaction month onwards till present month **/
		adjustMonthendBalanceSince(transaction.getFromAccountId(),transaction.getAmount().negate(),calendar.getCalendarId());
		/** Adjust balance of 'to' account from transaction month onwards till present month **/
		adjustMonthendBalanceSince(transaction.getToAccountId(),transaction.getAmount(),calendar.getCalendarId());
		return txn;
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public void deleteTransferTransaction(TransferTransaction transaction) {
		/** Delete the transaction **/
		transactionDAO.deleteTransferTransaction(transaction.getTxnId());
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
		/** Adjust balances for 'from' account from transaction month onwards **/
		adjustMonthendBalanceSince(transaction.getFromAccountId(),transaction.getAmount(),calendar.getCalendarId());
		/** Adjust balances for 'to' account from transaction month onwards **/
		adjustMonthendBalanceSince(transaction.getToAccountId(),transaction.getAmount().negate(),calendar.getCalendarId());
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRES_NEW)
	public void adjustTransferTransaction(TransferTransaction transaction) {
		//Find the existing transaction
		TransactionSearchCriteria criteria = new TransactionSearchCriteria();
		criteria.setTxnId(transaction.getTxnId());
		TransferTransaction existingTransaction = transactionDAO.getTransferTransactions(criteria).get(0);
		/** Find calendar id for transaction month **/
		MonthwiseCalendar calendar = getCalendarFor(existingTransaction.getTxnYear(), existingTransaction.getTxnMonth());
		/** If txn month or year has changed adjust account balances in 2 steps
		 *  1. Undo existing transaction amount since original transaction month & year account balances
		 *  2. Apply new transaction amount to account balances since changed transaction month and year 
		 *  Else apply differential amount to account balances since original transaction month & year account balances **/
		if ((transaction.getTxnMonth() != existingTransaction.getTxnMonth()) || (transaction.getTxnYear() != existingTransaction.getTxnYear())) {
			//Undo existing amount
			adjustMonthendBalanceSince(existingTransaction.getFromAccountId(),existingTransaction.getAmount(),calendar.getCalendarId());
			adjustMonthendBalanceSince(existingTransaction.getToAccountId(),existingTransaction.getAmount().negate(),calendar.getCalendarId());
			//Apply new amount
			calendar = getCalendarFor(transaction.getTxnYear(), transaction.getTxnMonth());
			adjustMonthendBalanceSince(transaction.getFromAccountId(),transaction.getAmount().negate(),calendar.getCalendarId());
			adjustMonthendBalanceSince(transaction.getToAccountId(),transaction.getAmount(),calendar.getCalendarId());
		} else {
			BigDecimal amountDiff = existingTransaction.getAmount().subtract(transaction.getAmount());
			adjustMonthendBalanceSince(existingTransaction.getFromAccountId(),amountDiff.negate(),calendar.getCalendarId());
			adjustMonthendBalanceSince(existingTransaction.getToAccountId(),amountDiff,calendar.getCalendarId());
		}
		/** save transaction **/
		transactionDAO.saveTransferTransaction(transaction);
	}

	@Override
	@Transactional(readOnly=true)
	public List<TransferTransaction> getTransferTransactions(TransactionSearchCriteria criteria) {
		return transactionDAO.getTransferTransactions(criteria);
	}
	
	/**
	 * Returns monthwise calendar for a given month and year. A new entry is created if one does not exist already
	 * @param year
	 * @param month
	 * @return MonthwiseCalendar.
	 */
	private MonthwiseCalendar getCalendarFor(Long year, Long month) {
		MonthwiseCalendar calendar = new MonthwiseCalendar();
		calendar.setMonth(month);
		calendar.setYear(year);
		MonthwiseCalendar managedCalendar = monthwiseCalendarDAO.getMonthwiseCalendars(calendar).get(0);
		//Save the calendar for the particular month if it does not exist. This is applicable for adding a transaction for the first time in any month
		if (managedCalendar == null) {
			managedCalendar = monthwiseCalendarDAO.saveMonthlyCalendar(calendar);
		}
		return managedCalendar;
	}
	
	/**
	 * Adjusts month end balances for an account for all months since the provided calendar month. A new entry is created if balance exists for that account for that calendar month.
	 * @param accountId. Account Id.
	 * @param amount. Signed differential amount to be adjusted
	 * @param calendarId. Calendar id corresponding to the month and year since when balances are to be adjusted.
	 */
	private void adjustMonthendBalanceSince(Long accountId, BigDecimal amount, Long calendarId) {
		MonthendAccountBalance balance = new MonthendAccountBalance();
		balance.setAccountId(accountId);
		balance.setBalance(amount);
		balance.setCalendarId(calendarId);
		int rows = accountDAO.adjustMonthendAccountBalanceSince(balance);
		//add a balance record for that month and account with the amount if no records exist already
		if (rows == 0) {
			accountDAO.saveMonthendAccountBalance(balance);
		}
	}
	
	/**
	 * Adjusts income expense category total for a differential. A new entry is created if no total exists for that category for that calendar month.
	 * @param categoryId. Income expense category id.
	 * @param calendarId. Calendar id for the month and year.
	 * @param amount. Amount to be adjusted. Provide signed differential to be adjusted.
	 */
	private void adjustMonthlyCategoryTotal(Long categoryId, Long calendarId, BigDecimal amount) {
		MonthlyCategoryTotal total = new MonthlyCategoryTotal();
		total.setCalendarId(calendarId);
		total.setIeCategoryId(categoryId);
		total.setTotal(amount);
		int rows = incomeExpenseDAO.adjustMonthlyCategoryTotal(total);
		//add a category total record for that month with the amount if no records exist already.
		if (rows == 0) {
			incomeExpenseDAO.saveMonthlyCategoryTotal(total);
		}
	}

}
